[Agent Builder] [SML] Add discovery_labels + autocomplete query path#1
[Agent Builder] [SML] Add discovery_labels + autocomplete query path#1Apmats wants to merge 6 commits into
Conversation
kderusso
left a comment
There was a problem hiding this comment.
Left some comments and questions, I know a lot will pend on the first PR :) Thanks for sharing!
| * Prefix typeahead / autocomplete is handled by a separate query path | ||
| * ({@link buildSmlAutocompleteQuery}) on a dedicated route. | ||
| * | ||
| * Search-API follow-up will refine this — field weighting, hybrid scoring tuning, etc. |
There was a problem hiding this comment.
Just curious, do we have an idea of what the overall strategy will be here?
| fields: [...SML_SEARCH_AS_YOU_TYPE_FIELDS], | ||
| }, | ||
| }, | ||
| { match: { title: trimmed } }, |
There was a problem hiding this comment.
Should we be using RRF or linear retrievers here?
There was a problem hiding this comment.
Coming in the separate actual-search-api PR.
| * Pick a highlight snippet from ES's per-subfield highlight object. | ||
| * Returns the first non-empty snippet; absent if none. | ||
| */ | ||
| const pickHighlightSnippet = ( |
There was a problem hiding this comment.
It would be interested to put in a discovery issue to see if we could use top snippets ;) Not required for MVP.
| inner_hits: { | ||
| _source: ['discovery_labels.value', 'discovery_labels.kind'], | ||
| size: 10, | ||
| highlight: { |
There was a problem hiding this comment.
We want to make sure to sort by score here ;) Also is default fragment size acceptable?
There was a problem hiding this comment.
Edited out the response - this was meant for the comment right below, apologies.
| type: 'bool_prefix', | ||
| operator: 'and', | ||
| fields: [ | ||
| 'discovery_labels.value', |
There was a problem hiding this comment.
Any boosts we should add here since we're doing multi match?
There was a problem hiding this comment.
So I had to read up on what exactly this bool_prefix query does. Concrete examples:
Doc "GitHub Open Source Connector" indexes as:
discovery_labels.value [github, open, source, connector]
discovery_labels.value._2gram [github open, open source, source connector]
discovery_labels.value._3gram [github open source, open source connector]
discovery_labels.value._index_prefix edge-ngrams of those shingles (plus tail-aligned bigram/unigram fallbacks)
Query "github open source c" rewrites to:
should[
( v._2gram:"github open" AND v._2gram:"open source" AND ConstantScore(v._index_prefix:"source c") ),
( v:github AND v:open AND v:source AND ConstantScore(v._index_prefix:"c") ),
( v._3gram:"github open source" AND ConstantScore(v._index_prefix:"open source c") ),
] ~minimum_should_match=1
So for each field, the last analyzed token (which might be a base unigram or a bigram/trigram shingle) is targeted at the _index_prefix subfield as a ConstantScore term query, while the previous tokens run as regular BM25 term queries against that subfield's own indexed data.
This means that naturally, docs whose indexed phrases align with the user's typed phrase as a prefix pick up extra ConstantScore clauses (each worth 1.0) and rank up without any significant boosting - the longer the shared prefix, the more clauses fire.
We do have some pathological cases though. For query "qqqq c" where qqqq is rare in the corpus (high IDF), a short doc "qqqq foo cabbage" (no "qqqq c" adjacency) outscores a long doc "qqqq cabbage some filler tokens here..." (which does have the bigram aligning with the typed prefix). Verified with a live experiment by Claude against ES: short doc scored 3.62, long doc 2.77. The BM25 swing on v:qqqq between the two was 2.62 - 0.77 = 1.85 - almost double the 1.0 ConstantScore bonus that should have protected the adjacency doc's rank. So when a base-term IDF is high enough and doc-length variance is big enough, BM25 noise can mess up the typeahead adjacency signal.
We can address this with just adding some boosts like so:
fields: [
'discovery_labels.value',
'discovery_labels.value._2gram^3',
'discovery_labels.value._3gram^5',
]multi_match field boosts are linear multipliers on every clause that subfield contributes - both the BM25 non-last-shingle ones and the ConstantScore last-shingle one. With ^3, the bigram constant lifts from 1.0 to 3.0; with ^5, the trigram from 1.0 to 5.0. Adjacency goes from "1.0 of headroom that one rare term can overcome" to "3–5 points of headroom that would need a near-pathological corpus to overcome."
Or some other arbitrary numbers...
|
|
||
| ## 8. Per-label provenance via `nested` | ||
|
|
||
| The schema PR makes `discovery_labels` a `nested` field with `{ value, kind }` per entry. This lets each SML record describe *what kind of label* a discovery term is — `tagline`, `nickname`, `category`, `synonym`, etc. — so the @ menu UI can render rows like: |
There was a problem hiding this comment.
Any concern with latency making this nested for autocomplete?
Edit: I see it's addressed a bit later in the doc, but is it worth testing?
There was a problem hiding this comment.
Hehe yeah very concerned.
Keep in mind though, baseline is we have the 2 default autocomplete inputs so not too many nested documents, and I don't see this number blowing up.
What made me move forward is that before going with this I asked Claude to do a quick benchmark and I was convinced it's not catastrophic.
I'll copy paste below.
So my full position is that I think this might be a very flexible and clean design that serves us well up to some point, but if we have certain ideas about how far SML will stretch we can test that concretely quite easily before we adopt this.
SML nested-autocomplete benchmark
Setup
- Index: 1 primary shard with 1 search-tier replica. Serverless requires
replicas >= 1to populate the search tier — withreplicas: 0every search returnsno_shard_available_action_exception. - Mapping: per spec; semantic fields (
unified_semantic,content,description) omitted because they require the inference endpoint and aren't what the SML autocomplete touches. - Data: synthetic docs with 2–5 word titles drawn from a realistic corpus (
Sales Q3 Dashboard,GitHub Connector,Production Logs Index, …),typefrom{dashboard, connector, visualization, index, lens, rule}, 3–8discovery_labelsper doc withkind∈{tagline, nickname, category, synonym}and 1–3 word values. - Loading: bulk batches of 1,000,
refresh=falseuntil the last batch which usedrefresh=wait_for, then_forcemerge?max_num_segments=1for stable timings. - Measurement: 5 warmup queries + 100 measured queries per prefix per scale, sequential single client. Reported
tookis server-side ES execution time. - Baseline: identical query with the
nestedshould-clause removed (only the multi_match againsttitle_autocomplete*+type.autocomplete*).
Results
| scale | prefix | nested p50 | nested p95 | nested p99 | base p50 | base p95 | base p99 | absolute overhead (p95) |
|---|---|---|---|---|---|---|---|---|
| 1,000 | a |
4 ms | 7 ms | 9 ms | 1 ms | 1 ms | 2 ms | +6 ms |
| 1,000 | sa |
2 | 5 | 7 | 1 | 1 | 2 | +4 |
| 1,000 | git |
2 | 3 | 3 | 0 | 1 | 1 | +2 |
| 1,000 | das |
1 | 1 | 2 | 0 | 1 | 1 | 0 |
| 1,000 | production l |
1 | 2 | 3 | 0 | 1 | 1 | +1 |
| 10,000 | a |
1 | 1 | 2 | 0 | 1 | 1 | 0 |
| 10,000 | sa |
1 | 1 | 2 | 0 | 0 | 1 | +1 |
| 10,000 | git |
1 | 1 | 2 | 0 | 1 | 1 | 0 |
| 10,000 | das |
1 | 1 | 2 | 0 | 1 | 1 | 0 |
| 10,000 | production l |
1 | 2 | 2 | 0 | 1 | 1 | +1 |
| 100,000 | a |
3 | 4 | 5 | 0 | 0 | 0 | +4 |
| 100,000 | sa |
1 | 2 | 2 | 0 | 1 | 1 | +1 |
| 100,000 | git |
1 | 2 | 2 | 0 | 0 | 1 | +2 |
| 100,000 | das |
1 | 1 | 1 | 0 | 1 | 1 | 0 |
| 100,000 | production l |
3 | 5 | 5 | 1 | 1 | 2 | +4 |
Relative-overhead percentages are noisy because the baseline routinely rounds to 0–1 ms; absolute deltas (last column) are the honest measure. Worst single point: nested p99 = 9 ms at 1k for the bare-"a" prefix.
Correctness sanity ✓
Indexed {discovery_labels: [{value:"github",kind:"tagline"},{value:"gh",kind:"nickname"}]}, queried "git". Response:
hits.hits[0].matched_queries = ["discovery_label_prefix"]inner_hits.discovery_labels.hits.hits[0]._source = {value: "github", kind: "tagline"}— matches spec.
Note: inner_hits is keyed by the nested path (discovery_labels), not by the _name of the nested query (discovery_label_prefix). The harness's first assertion looked under the wrong key and reported a false FAIL; the actual ES response is correct.
Verdict
At 100k records, the nested clause's overhead is fine for a typeahead UX. Nested p95 ≤ 5 ms and p99 ≤ 5 ms across all tested prefixes — well under the 100 ms target. The added cost on top of the title-only baseline is on the order of 1–5 ms, which is invisible to a user.
Flags / caveats
- Single-shard, single-client, no concurrent load. Real Kibana traffic will run wider; expect more variance under load but no structural reason the nested clause should degrade differently than the parent
multi_match. tookresolution is 1 ms. Baseline values frequently round to 0, so the percentage-overhead column is misleading — use the absolute-overhead column.track_total_hitssaturates at 10,000 at the 100k scale (default cap). Does not affect query latency or the top-10 hits returned; if any caller readshits.total.valueexpecting a true count, passtrack_total_hits: true. For an autocomplete that only consumes top N, the default is fine.inner_hits.size: 10was never near a limit here (each doc has ≤8 labels). Worth re-testing if real-world docs grow to dozens of labels.- Mapping cost.
search_as_you_typeon bothtitle_autocompleteanddiscovery_labels.valuematerialises three subfields (_2gram,_3gram,_index_prefix) per field. Store size was reported as0bby_stats(serverless reportsdataset.sizeinstead), so this run can't put a number on it, but the multi_match queries 6+ fields per call and that is where the modest nested-vs-baseline gap comes from. Worth profiling if the index grows to 1M+ docs. - No heap or segment pressure observed. 100k load took ~25 s; one segment after force-merge.
Cleanup
Both sml_bench_v1 and the side-debug sml_correctness_v1 index were deleted. Verified with GET /_cat/indices/sml_* returning empty.
Artifacts
/tmp/sml_bench/bench.py— benchmark harness/tmp/sml_bench/results.json— raw per-prefixtookarrays/tmp/sml_bench/bench.log— full stdout from the run
92250c4 to
95f51b6
Compare
Adds the @ menu / typeahead query path on top of the schema PR. The
autocomplete surface is unified through `discovery_labels` — title and
type are no longer special-cased.
discovery_labels: nested { value, kind }
- `value.autocomplete` is a `text` field with a custom edge-ngram analyzer
(NOT `search_as_you_type` — see WHY below).
- The indexer auto-prepends `{value: chunk.title, kind: 'title'}` and
`{value: chunk.type, kind: 'type'}` to every record, plus any entries
the producer provides (taglines, nicknames, categories, etc.). Type
writers don't change their SmlChunk shape.
Drops `title_autocomplete` (top-level SAYT field) and the `.autocomplete`
subfield from `type`. Title's and type's autocomplete reach now goes
through their auto-prepended discovery_labels entries; `type` stays as
plain keyword for exact filtering.
POST /internal/agent_context_layer/sml/_autocomplete — dedicated route
for the @ menu, separate from /sml/_search.
buildSmlAutocompleteQuery: a single nested query against
discovery_labels.value.autocomplete with `match` + `operator: and` (every
typed token must match). `inner_hits` returns the matched entries with
their `kind` and an ES `unified`-highlighter snippet wrapping the
matched word(s) in <em>...</em>.
Response: { id, type, title, origin_id, matched_discovery_labels? }.
No content, no description, no score — order is sufficient; UI gets
exactly what it needs to render a typeahead row with badges.
filterResultsByPermissions is generic over the result type and shared
with the search path.
SAYT (`type: 'search_as_you_type'`) is the obvious first choice for an
autocomplete field. It does not produce useful highlights for prefix
queries in a nested context. Confirmed in two ways:
1. Empirical: tested unified, plain, fvh highlighters; tested
`require_field_match: false`, `highlight_query: prefix`, term_vector
with offsets, top-level vs inner_hits highlight — none returned a
snippet for nested + SAYT + bool_prefix on prefix-only matches.
2. Documented: this is the bug behind elastic/elasticsearch#53744
("search_as_you_type, prefix queries and broken highlighting"),
open since 2020-03, explicitly low-priority per the ES Search team
lead at the time. Root cause documented in elastic#49069 ("Highlight with
index_prefixes"): SAYT's `_index_prefix` subfield indexes
position-spanning ngrams with offsets that span from the prefix
start to the end of the entire field, which the highlighter can't
wrap per-word. The ES-community workaround (see
elastic/elasticsearch#53744 comment by peterbe) is exactly what
this PR does: use a regular text field with a custom analyzer.
The custom analyzer: standard tokenizer + lowercase + edge_ngram filter
(1–20) at index time; standard + lowercase at search time. Per-token
edge_ngrams preserve each word's offsets, so the highlighter wraps the
matched word correctly — verified live for queries like "git" →
"<em>github</em>", "github c" → "<em>GitHub</em> <em>Connector</em>",
"prod logs ana" → "<em>production</em> <em>logs</em> <em>analytics</em>".
Trade-offs vs SAYT:
- Smaller inverted index (one field, not four).
- Comparable query latency (posting-list lookup per token).
- Loses SAYT's _2gram/_3gram shingle subfields used for phrase-prefix
scoring. BM25 on edge_ngram tokens is good enough for typeahead
ranking.
Adds an optional `analysis` field to `IndexStorageSettings`, threaded
through to the index template's `settings.analysis` block. Required so
that consumers (like SML here) can declare custom analyzers without
bypassing the storage adapter's bootstrap flow. Backwards-compatible —
field is optional. Ignored on serverless ES (no index-level settings).
Owners: @elastic/observability-ui, @elastic/kibana-core.
- Service test: query shape (match + operator: and + nested + inner_hits
+ highlight), inner_hits mapping into matched_discovery_labels,
highlight passthrough.
- Route test: response shape, no permissions/spaces/content leak.
- FTR (real-ES): /sml/_autocomplete against an indexed record with
title / type / tagline discovery_labels; asserts matched_discovery_labels,
kind, and that the highlight wraps the matched word.
- 121/121 jest in agent_context_layer; 14/14 jest in kbn-storage-adapter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacks on 03da4f4 (which introduced custom edge_ngram analyzer for discovery_labels.value.autocomplete to get nested-context highlights working). The edge_ngram approach highlights correctly but is loose: every typed token behaves as a prefix at index time, so "github c" matches "Githubster Cup" (because "github" is an indexed edge_ngram of "githubster"). Verified live. This commit pivots discovery_labels.value to `search_as_you_type` with `multi_match bool_prefix operator:and`. SAYT's native query type gives tight per-token semantics: all-but-last analyzed tokens require exact indexed-term match (against ES's auto-generated _2gram/_3gram shingle subfields); only the last (trailing partial) is treated as a prefix (against _index_prefix). Net: "github c" no longer matches "Githubster Cup". Trade-off: highlights. SAYT + bool_prefix + nested + inner_hits is the exact case behind ES bug elastic/elasticsearch#53744 (open since 2020), so `matched_discovery_labels` entries return without `highlighted`. The route still configures the highlighter (forward-compat when the bug lands); UI falls back to rendering plain `value` when `highlighted` is absent. Alternative considered (not taken in this PR): hybrid AND — bool { must: [match(edge_ngram), match_bool_prefix(plain text)] } Live-tested to give tight semantics AND working highlights (from the edge_ngram clause). Costs an extra plain-text subfield on `discovery_labels.value` (~5% index footprint with index_options:freqs norms:false) and still has the "git c" mid-typing gap where match_bool_prefix demands a complete indexed token. Slotted for PR3 if user feedback warrants. Removes the custom edge_ngram analyzers and the storageSettings `analysis` block — SAYT needs no custom analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt connector filter support
Two changes that ship together because they touch the same FE surface:
1. Wire the @ menu typeahead to POST /sml/_autocomplete (instead of
the previously used POST /sml/_search). Adds:
- `smlService.autocomplete({ query, size, filters? })`
- `queryKeys.sml.autocomplete(query, filters?)`
- `useSmlAutocomplete(query, { filters? })` hook
- `render_sml_highlight.tsx` helper that parses ES highlight snippets
(`<em>...</em>`) into React fragments — currently inert on the SAYT
path because of ES bug elastic#53744, forward-compatible when it lands.
- `Sml` component switches to the new hook and renders per-row
`matched_discovery_labels` (`kind`-aware) instead of client-side
`EuiHighlight` on `${type}/${title}`.
- Drops `sml_command_menu_highlight.{ts,test.ts}` — the old
client-side highlight parser is no longer needed.
2. Restore agent-centric connector scoping (Sean's
`agent-centric connectors` feature, elastic#267333) on the autocomplete
route. The earlier autocomplete commit (03da4f4) added the route
without a `filters` parameter, which silently broke the @ menu's
respect for `agent.configuration.connector_ids`. This commit:
- BE: adds `filters?: SmlSearchFilters` to the autocomplete route
schema and threads it through `SmlService.autocomplete` and
`autocompleteSml`, which now calls the existing `buildTypeFilters`
helper (shared with the search path) so the filter clause is
identical between routes.
- FE: `useSmlAutocomplete` accepts `{ filters? }`,
`usePrefetchSml(filters?)` regains the filter param, the
command-menu orchestrator re-prefetches SML when the agent's
filters change after the async agent fetch resolves.
- Tests: new BE unit test asserting filters flow into the ES filter
clauses via `buildTypeFilters`; new FE hook test covering the
filters arg; existing `sml.test.tsx` + `use_prefetch_sml.test.tsx`
updated to the new mocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…@ menu The previous commit on this branch (b46c879) rewrote the @ menu rendering to use the server's `matched_discovery_labels` and a custom `<em>...</em>` parser. That assumed working server-side highlights — true for the edge_ngram iteration, but the SAYT pivot (202ce94) makes ES return no highlight snippets (elastic#53744), so the new path renders plain text with no visual indication of what matched. The new `SmlAutocompleteHttpResultItem` still has `type` and `title` top-level, which is everything the original client-side EuiHighlight rendering needed. Restoring it gets us back the "bold the typed query in the displayed row" UX with no schema or route changes: - `sml.tsx` back to two `<EuiHighlight>` blocks over `type` and `title`, using the same `getSmlMenuHighlightSearchStrings` parser. - Restored `sml_command_menu_highlight.{ts,test.ts}`. - Dropped `render_sml_highlight.tsx` — no longer used. (Forward-compat for elastic#53744 isn't load-bearing; whoever revives server-side highlights can write it then.) - `matched_discovery_labels` is still returned by the BE and still on the FE result type — FE engineers can switch the rendering to use it whenever they want kind-aware badges. Hook + filter wiring (useSmlAutocomplete with agent connector filters) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier autocomplete commit on this branch added an optional `analysis` field to `IndexStorageSettings` so the (then-planned) custom edge_ngram analyzer could be declared through the storage adapter. The SAYT pivot on the following commit dropped the custom analyzer entirely — SAYT brings its own auto-generated subfields and doesn't need index-level `analysis`. `grep` confirms no SML file references `analysis` today, and no other consumer in the tree was planning to use the hook. Reverting the two storage-adapter files to their main-branch state removes ~18 lines of unused code and drops `@elastic/observability-ui` / `@elastic/kibana-core` from the PR's required reviewers, since this PR no longer touches their package. If a future consumer needs to declare custom analyzers through the storage adapter, the hook can be reintroduced then with a real motivating use case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… abandoned iterations Two doc-comment blocks (one in sml_storage's discovery_labels mapping, one in sml_service's buildSmlAutocompleteQuery) carried a tail referring to the custom edge_ngram approach and hybrid-AND alternative — iterations we tried during this PR and did not ship. These comments rot the moment the PR closes: the "PR description" reference becomes opaque, and the "earlier commit" comparison no longer maps to anything visible in the file. The technical content stays: SAYT mechanics, the elastic#53744 limitation, what `matched_discovery_labels.highlighted` means. That's still useful for future readers and isn't visible from the code alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6dfc43f to
3d81427
Compare
TL;DR
Schema:
discovery_labelsbecomesnested { value, kind }withvalueassearch_as_you_type. Title and type are auto-prepended todiscovery_labelsat index time by the SML indexer. The dedicatedtitle_autocompletefield and thetype.autocompletesubfield are dropped — they're no longer needed.New route:
POST /internal/agent_context_layer/sml/_autocomplete— dedicated to the @ menu / typeahead. Separate from the existingPOST /sml/_search(full retrieval for the LLM tool). Each route owns its query shape; the @ menu and the LLM tool stop fighting over a unified codepath.Query: a single nested
multi_match bool_prefix operator: andagainstdiscovery_labels.valueand its auto-generated_2gram/_3gramshingle subfields. All-but-last analyzed tokens must match indexed terms exactly; only the trailing partial behaves as a prefix. Per-nested-entryoperator: andenforcement means typed tokens must all land on the same entry.Response shape (live-tested) — indexed record
{ type: 'connector', title: 'GitHub Connector' }with a producer-supplied[{ value: 'github', kind: 'tagline' }]. After the indexer auto-prepends title and type, the record'sdiscovery_labelshold three entries: titleGitHub Connector, typeconnector, taglinegithub. The route returns the entries that satisfied the query asmatched_discovery_labelsin ES score order.POST /sml/_autocompletewith body{ "query": "git" }—"git"is a prefix (last/only token), so it prefix-matches all entries whose tokens start withgit. Title and tagline contain such a token; typeconnectordoes not.{ "total": 1, "results": [{ "id": "connector:gh-1:abc", "type": "connector", "title": "GitHub Connector", "origin_id": "gh-1", "matched_discovery_labels": [ { "value": "github", "kind": "tagline" }, { "value": "GitHub Connector", "kind": "title" } ] }] }POST /sml/_autocompletewith body{ "query": "github c" }—"github"is now a confirmed (non-trailing) token and must match an indexed term exactly;"c"is the trailing partial and may match as a prefix. Only the title entry has both: an exactgithubtoken and ac-prefixtoken (Connector). The tagline lacksc-prefix; the type lacksgithub.{ "total": 1, "results": [{ "id": "connector:gh-1:abc", "type": "connector", "title": "GitHub Connector", "origin_id": "gh-1", "matched_discovery_labels": [ { "value": "GitHub Connector", "kind": "title" } ] }] }matched_discovery_labelsships as a server capability for the UI to lean on when ready. Today's @ menu doesn't render it yet — rows still display as${type}/${title}with client-sideEuiHighlightbolding the typed query (the existing UX), and the new field is available for future iteration intokind-aware badges.highlightedis intentionally absent on entries returned by this route — see "Known limitation: no highlights" below.Bonus —
kbn-storage-adapter: gains an optionalanalysisfield onIndexStorageSettings, threaded through to the index template'ssettings.analysis. Added by the earlier commit on this branch for the edge_ngram pivot; retained as a generic capability so future consumers can declare custom analyzers without bypassing the adapter's bootstrap flow. Backwards-compatible. Ignored on serverless. Owners: @elastic/observability-ui, @elastic/kibana-core.Known limitation: no highlights from this route (ES bug elastic#53744)
SAYT +
bool_prefix+ nested +inner_hitshighlight is the exact configuration tracked in elastic/elasticsearch#53744 ("search_as_you_type, prefix queries and broken highlighting"), open since 2020-03 and explicitly low-priority per the ES Search team. Root cause documented in elastic/elasticsearch#49069: SAYT's_index_prefixsubfield indexes position-spanning ngrams with offsets that span from the prefix start to the end of the entire field — the highlighter can't wrap per-word.We keep the highlight config in the request (forward-compatible for when the bug is fixed) but ES returns no snippet, so
matched_discovery_labelsentries arrive withouthighlighted. The @ menu UI continues to bold the typed query client-side viaEuiHighlighton the displayedtype/title— that part is unchanged from the pre-PR UX, so the user always sees which substring matched, with or without server-side highlights.Why we accept this rather than work around it
prefixquery directly: rewritten internally to aboolof term queries over every indexed term sharing the prefix. Expensive at query time; the whole point of SAYT/edge_ngram is to move that work to index time.Custom edge_ngram analyzer (commit 1 of this branch): highlights cleanly but produces loose matches across confirmed tokens. Worse typeahead UX than no highlight.
Hybrid AND (live-tested but not taken):
Tight matching from the
match_bool_prefixclause + working highlights from the edge_ngram clause (the highlighter doesn't break on amatchagainst an edge_ngram-analyzed plain text field). Costs an extra subfield ondiscovery_labels.value(withindex_options: freqs, norms: falseit's marginal — <5% of the discovery_labels postings size). Adds dual scoring. Mid-typing UX gap remains:"git c"returns 0 because"git"isn't a complete indexed token.Slotted for PR3 if real users feel the missing highlights — at that point we'd also have data on whether the mid-typing gap is a real problem.
For PR2 we ship the simpler, tighter SAYT path and accept no highlights as a documented limitation.
Schema
discovery_labels: nested { value, kind }valueissearch_as_you_type. ES auto-generates_2gram,_3gram, and_index_prefixsubfields used bybool_prefix.{value: chunk.title, kind: 'title'}and{value: chunk.type, kind: 'type'}to every record, plus any entries the producer provides (taglines, nicknames, categories, etc.). Type writers don't change theirSmlChunkshape.Drops
title_autocomplete(top-level SAYT field) and the.autocompletesubfield fromtype. Title's and type's autocomplete reach now goes through their auto-prependeddiscovery_labelsentries;typestays as plainkeywordfor exact filtering.Route + service
POST /internal/agent_context_layer/sml/_autocomplete— dedicated route for the @ menu, separate from/sml/_search.buildSmlAutocompleteQuery: a single nestedmulti_match bool_prefix operator: andagainstdiscovery_labels.valueand its_2gram/_3gramsubfields.inner_hitsreturns the matched entries with theirkind.operator: andis evaluated per nested entry, so an entry that contains only a subset of the typed tokens is excluded — see the"github c"example above where neither the type entry nor the tagline entry is returned (the type lacks agithub-token; the tagline lacks ac-prefix-token).Response:
{ id, type, title, origin_id, matched_discovery_labels? }. No content, no description, no score — order is sufficient. The @ menu today renders${type}/${title}withEuiHighlightclient-side bolding (same as pre-PR);matched_discovery_labelsis server-side capability ready for FE engineers to adopt when they wantkind-aware row badges.The route accepts the same
filters?: SmlSearchFiltersbody parameter as/sml/_search— same shape, samebuildTypeFiltershelper, same per-type allow-list semantics. This is what restores the agent-centric connector scoping (#267333) for the @ menu: an agent withconfiguration.connector_ids: [...]sees only the listed connectors in the typeahead. Other SML types are unaffected.filterResultsByPermissionsis generic over the result type and shared with the search path.kbn-storage-adapterchangeAdds an optional
analysisfield toIndexStorageSettings, threaded through to the index template'ssettings.analysisblock. Introduced in commit 1 for the edge_ngram pivot; SML no longer uses it after commit 2's switch to SAYT, but the storage-adapter capability is retained as it's broadly useful for future consumers that need to declare custom analyzers without bypassing the adapter's bootstrap flow. Backwards-compatible — field is optional. Ignored on serverless ES (no index-level settings).Owners: @elastic/observability-ui, @elastic/kibana-core.
Test plan
node scripts/eslint --fixon touched files.node scripts/type_check --project x-pack/platform/plugins/shared/agent_context_layer/tsconfig.json.node scripts/jest x-pack/platform/plugins/shared/agent_context_layer/server/services/sml/— all pass.node scripts/jest src/platform/packages/shared/kbn-storage-adapter/src/index_adapter/— all pass.node scripts/jest x-pack/platform/plugins/shared/agent_builder/public/application/components/conversations/conversation_input/message_editor/command_menu/menus/sml/—sml.test.tsx+use_prefetch_sml.test.tsxcover the autocomplete + filter wiring.node scripts/jest x-pack/platform/plugins/shared/agent_builder/public/application/hooks/sml/use_sml_autocomplete.test.tsx— new hook test, including the filters arg.agent_builder_api_integrationSML autocomplete tests exercise the route end-to-end against indexeddiscovery_labelsentries; assertmatched_discovery_labelsandkind.highlightedis asserted absent (documents the WIP: Move agg_types into data shim plugin elastic/kibana#53744 known limitation).connector_ids, re-open the menu — only the listed connectors appear.